package com.gitee.wsl.compose.ui.richtext.edit.state


import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.util.fastForEachIndexed
import com.gitee.wsl.compose.ui.richtext.edit.annotation.ExperimentalRichTextApi
import com.gitee.wsl.compose.ui.richtext.edit.append
import com.gitee.wsl.compose.ext.text.customMerge
import com.gitee.wsl.compose.ui.richtext.edit.model.span.RichSpan
import com.gitee.wsl.compose.ui.richtext.edit.model.span.style.RichSpanStyle
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.RichParagraph
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.DefaultParagraphType
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.list.OrderedList
import com.gitee.wsl.compose.ext.text.unmerge
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.api.ConfigurableListLevel
import com.gitee.wsl.compose.ui.richtext.edit.model.paragraph.type.startText
import kotlin.math.max


/**
 * Handles the new text field value.
 *
 * @param newTextFieldValue the new text field value.
 */
internal fun RichTextState.onTextFieldValueChange(newTextFieldValue: TextFieldValue) {
    tempTextFieldValue = newTextFieldValue

    if (tempTextFieldValue.text.length > textFieldValue.text.length)
        handleAddingCharacters()
    else if (tempTextFieldValue.text.length < textFieldValue.text.length)
        handleRemovingCharacters()
    else if (isOnlySelectionChanged()) {
        val lastPressPosition = this.lastPressPosition
        if (lastPressPosition != null) {
            adjustSelection(lastPressPosition, newTextFieldValue.selection)
            return
        }
    }

    // Update text field value
    updateTextFieldValue()
}

/**
 * Handles updating the text field value and all the related states such as the [annotatedString] and [visualTransformation] to reflect the new text field value.
 *
 * @param newTextFieldValue the new text field value.
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.updateTextFieldValue(newTextFieldValue: TextFieldValue = tempTextFieldValue) {
    tempTextFieldValue = newTextFieldValue

    if (!singleParagraphMode) {
        // Check for paragraphs
        checkForParagraphs()
    }

    if (isOnlySelectionChanged()) {
        // Update selection
        textFieldValue = tempTextFieldValue
    } else {
        // Update the annotatedString and the textFieldValue with the new values
        updateAnnotatedString(tempTextFieldValue)
    }

    // Clear un-applied styles
    toAddSpanStyle =  RichSpanStyle.DefaultSpanStyle
    toRemoveSpanStyle = RichSpanStyle.DefaultSpanStyle
    toAddRichSpanStyle = RichSpanStyle.Default
    toRemoveRichSpanStyle = RichSpanStyle.Default::class

    // Update current span style
    updateCurrentSpanStyle()

    // Update current paragraph style
    updateCurrentParagraphStyle()

    // Clear [tempTextFieldValue]
    tempTextFieldValue = TextFieldValue()
}

/**
 * Update the [annotatedString] to reflect the new changes on the [richParagraphList].
 * This method will update the [annotatedString] and the [textFieldValue] to reflect the new changes.
 * If no [newTextFieldValue] is passed, the [textFieldValue] will be used instead.
 *
 * @param newTextFieldValue the new text field value.
 * @see [textFieldValue]
 * @see [annotatedString]
 */
@OptIn(ExperimentalRichTextApi::class)
internal fun RichTextState.updateAnnotatedString(newTextFieldValue: TextFieldValue = textFieldValue) {
    val newText =
        if (singleParagraphMode)
            newTextFieldValue.text
        else
            newTextFieldValue.text.replace('\n', ' ')

    val newStyledRichSpanList = mutableListOf<RichSpan>()

    usedInlineContentMapKeys.clear()

    annotatedString = buildAnnotatedString {
        var index = 0
        richParagraphList.fastForEachIndexed { i, richParagraph ->
            if (index > newText.length) {
                richParagraphList.removeAt(i)
                return@fastForEachIndexed
            }

            withStyle(richParagraph.paragraphStyle.merge(richParagraph.type.getStyle(config))) {
                withStyle(richParagraph.getStartTextSpanStyle() ?: RichSpanStyle.DefaultSpanStyle) {
                    append(richParagraph.type.startText)
                }
                val richParagraphStartTextLength = richParagraph.type.startText.length
                richParagraph.type.startRichSpan.textRange = TextRange(index, index + richParagraphStartTextLength)
                index += richParagraphStartTextLength
                withStyle(RichSpanStyle.DefaultSpanStyle) {
                    index = append(
                        state = this@updateAnnotatedString,
                        richSpanList = richParagraph.children,
                        startIndex = index,
                        text = newText,
                        selection = newTextFieldValue.selection,
                        onStyledRichSpan = {
                            newStyledRichSpanList.add(it)
                        },
                    )

                    if (!singleParagraphMode) {
                        // Add empty space in the end of each paragraph to fix an issue with Compose TextField
                        // that makes that last char non-selectable when having multiple paragraphs
                        if (i != richParagraphList.lastIndex && index < newText.length) {
                            append(' ')
                            index++
                        }
                    }
                }
            }
        }
    }

    inlineContentMap.keys.forEach { key ->
        if (key !in usedInlineContentMapKeys) {
            inlineContentMap.remove(key)
        }
    }

    styledRichSpanList.clear()
    textFieldValue = newTextFieldValue.copy(text = annotatedString.text)
    visualTransformation = VisualTransformation { _ ->
        TransformedText(
            text = annotatedString,
            offsetMapping = OffsetMapping.Identity
        )
    }
    styledRichSpanList.addAll(newStyledRichSpanList)
}
/**
 * Handles adding characters to the text field.
 * This method will update the [richParagraphList] to reflect the new changes.
 * This method will use the [tempTextFieldValue] to get the new characters.
 */
@OptIn(ExperimentalRichTextApi::class)
private fun RichTextState.handleAddingCharacters() {
    val typedCharsCount = tempTextFieldValue.text.length - textFieldValue.text.length
    var startTypeIndex = textFieldValue.selection.min
    val typedText = tempTextFieldValue.text.substring(
        startIndex = startTypeIndex,
        endIndex = startTypeIndex + typedCharsCount,
    )
    val previousIndex = startTypeIndex - 1

    val activeRichSpan = getOrCreateRichSpanByTextIndex(previousIndex)

    if (activeRichSpan != null) {
        val isAndroidSuggestion =
            activeRichSpan.isLastInParagraph &&
                    activeRichSpan.textRange.max == startTypeIndex &&
                    tempTextFieldValue.selection.max == startTypeIndex + typedCharsCount + 1

        val typedText =
            if (isAndroidSuggestion)
                "$typedText "
            else
                typedText

        if (isAndroidSuggestion) {
            val beforeText =
                tempTextFieldValue.text.substring(0, startTypeIndex + typedCharsCount)

            val afterText =
                tempTextFieldValue.text.substring(startTypeIndex + typedCharsCount)

            tempTextFieldValue = tempTextFieldValue.copy(
                text = "$beforeText $afterText",
            )
        }

        if (startTypeIndex < activeRichSpan.textRange.min) {
            val indexDiff = activeRichSpan.textRange.min - startTypeIndex
            val beforeTypedText = tempTextFieldValue.text.substring(
                startIndex = 0,
                endIndex = startTypeIndex,
            )
            val paragraphStartText = tempTextFieldValue.text.substring(
                startIndex = startTypeIndex + typedCharsCount,
                endIndex = activeRichSpan.textRange.min + typedCharsCount,
            )
            val afterTypedText = tempTextFieldValue.text.substring(
                startIndex = activeRichSpan.textRange.min + typedCharsCount,
                endIndex = tempTextFieldValue.text.length,
            )
            val newTypedText = beforeTypedText + paragraphStartText + typedText + afterTypedText
            tempTextFieldValue = tempTextFieldValue.copy(
                text = newTypedText,
                selection = TextRange(
                    (tempTextFieldValue.selection.min + indexDiff).coerceAtLeast(0),
                    (tempTextFieldValue.selection.max + indexDiff).coerceAtMost(newTypedText.length),
                ),
            )
        }

        startTypeIndex = max(startTypeIndex, activeRichSpan.textRange.min)
        val startIndex = max(0, startTypeIndex - activeRichSpan.textRange.min)
        val beforeText =
            if (activeRichSpan.text.isEmpty())
                ""
            else
                activeRichSpan.text.substring(0, startIndex)

        val afterText =
            if (activeRichSpan.text.isEmpty())
                ""
            else
                activeRichSpan.text.substring(startIndex)

        val activeRichSpanFullSpanStyle = activeRichSpan.fullSpanStyle
        val newSpanStyle =
            activeRichSpanFullSpanStyle.customMerge(toAddSpanStyle).unmerge(toRemoveSpanStyle)
        val newRichSpanStyle =
            when {
                toAddRichSpanStyle != RichSpanStyle.Default -> toAddRichSpanStyle

                toRemoveRichSpanStyleKClass == activeRichSpan.richSpanStyle::class -> RichSpanStyle.Default

                else -> activeRichSpan.richSpanStyle
            }

        val isToAddRemoveSpanStyleEmpty =
            toAddSpanStyle == SpanStyle() && toRemoveSpanStyle == SpanStyle()

        val isToAddRemoveRichSpanStyleEmpty =
            toAddRichSpanStyle == RichSpanStyle.Default && toRemoveRichSpanStyleKClass == RichSpanStyle.Default::class

        if (
            (isToAddRemoveSpanStyleEmpty && isToAddRemoveRichSpanStyleEmpty) ||
            (newSpanStyle == activeRichSpanFullSpanStyle && newRichSpanStyle::class == activeRichSpan.richSpanStyle::class)
        ) {
            activeRichSpan.text = beforeText + typedText + afterText

            checkListStart(richSpan = activeRichSpan)
        } else {
            handleUpdatingRichSpan(
                richSpan = activeRichSpan,
                beforeText = beforeText,
                middleText = typedText,
                afterText = afterText,
                startIndex = startTypeIndex,
                richSpanFullSpanStyle = activeRichSpanFullSpanStyle,
                newSpanStyle = newSpanStyle,
            )
        }
    } else {
        if (richParagraphList.isEmpty()) {
            richParagraphList.add(RichParagraph())
        }

        val newRichSpan = RichSpan(
            paragraph = richParagraphList.last(),
            text = typedText,
            textRange = TextRange(startTypeIndex, startTypeIndex + typedText.length),
            spanStyle = toAddSpanStyle,
            richSpanStyle = toAddRichSpanStyle,
        )
        richParagraphList.last().children.add(newRichSpan)
    }
}

/**
 * Handles removing characters from the text field value.
 * This method will update the [richParagraphList] to reflect the new changes.
 * This method will use the [tempTextFieldValue] to get the removed characters.
 */
internal fun RichTextState.handleRemovingCharacters() {
    val removedCharsCount = textFieldValue.text.length - tempTextFieldValue.text.length
    val minRemoveIndex = tempTextFieldValue.selection.min
    val maxRemoveIndex = tempTextFieldValue.selection.min + removedCharsCount
    val removeRange = TextRange(minRemoveIndex, maxRemoveIndex)

    val minRichSpan = getRichSpanByTextIndex(textIndex = minRemoveIndex, true) ?: return
    val maxRichSpan = getRichSpanByTextIndex(textIndex = maxRemoveIndex - 1, true) ?: return

    // Check deleted paragraphs
    val minParagraphIndex = richParagraphList.indexOf(minRichSpan.paragraph)
    val maxParagraphIndex = richParagraphList.indexOf(maxRichSpan.paragraph)

    // Remove paragraphs between the min and max paragraphs
    if (minParagraphIndex < maxParagraphIndex - 1 && !singleParagraphMode) {
        richParagraphList.removeRange(minParagraphIndex + 1, maxParagraphIndex)
    }

    // Get the first non-empty child of the min paragraph
    val minFirstNonEmptyChild = minRichSpan.paragraph.getFirstNonEmptyChild()
    val minParagraphStartTextLength = minRichSpan.paragraph.type.startRichSpan.text.length
    val minParagraphFirstChildMinIndex = minFirstNonEmptyChild?.textRange?.min ?: minParagraphStartTextLength

    // Get the first non-empty child of the max paragraph
    val maxFirstNonEmptyChild = maxRichSpan.paragraph.getFirstNonEmptyChild()
    val maxParagraphStartTextLength = maxRichSpan.paragraph.type.startRichSpan.text.length
    val maxParagraphFirstChildMinIndex = maxFirstNonEmptyChild?.textRange?.min ?: maxParagraphStartTextLength

    if (minParagraphIndex == maxParagraphIndex && !singleParagraphMode) {
        if (minFirstNonEmptyChild == null || minFirstNonEmptyChild.text.isEmpty()) {
            if (minRichSpan.paragraph.type.startText.isEmpty()) {
                // Remove the min paragraph if it's empty (and the max paragraph is the same)
                richParagraphList.removeAt(minParagraphIndex)
            }
        }
    }

    // Handle Remove the min paragraph custom text
    if (minRemoveIndex < minParagraphFirstChildMinIndex) {
        handleRemoveMinParagraphStartText(
            removeIndex = minRemoveIndex,
            paragraphStartTextLength = minParagraphStartTextLength,
            paragraphFirstChildMinIndex = minParagraphFirstChildMinIndex,
        )

        minRichSpan.paragraph.type = DefaultParagraphType()

        tempTextFieldValue = adjustOrderedListsNumbers(
            startParagraphIndex = minParagraphIndex + 1,
            startNumber = 1,
            textFieldValue = tempTextFieldValue,
        )
    }

    // Handle Remove the max paragraph custom text
    if (maxRemoveIndex < maxParagraphFirstChildMinIndex) {
        handleRemoveMaxParagraphStartText(
            minRemoveIndex = minRemoveIndex,
            maxRemoveIndex = maxRemoveIndex,
            paragraphStartTextLength = maxParagraphStartTextLength,
            paragraphFirstChildMinIndex = maxParagraphFirstChildMinIndex,
        )

        maxRichSpan.paragraph.type = DefaultParagraphType()

        tempTextFieldValue = adjustOrderedListsNumbers(
            startParagraphIndex = maxParagraphIndex + 1,
            startNumber = 1,
            textFieldValue = tempTextFieldValue,
        )
    }

    // Remove spans from the max paragraph
    maxRichSpan.paragraph.removeTextRange(removeRange, maxParagraphFirstChildMinIndex)

    if (!singleParagraphMode) {
        if (maxParagraphIndex != minParagraphIndex) {
            // Remove spans from the min paragraph
            minRichSpan.paragraph.removeTextRange(removeRange, minParagraphFirstChildMinIndex)

            if (maxRichSpan.paragraph.getFirstNonEmptyChild() == null) {
                // Remove the max paragraph if it's empty
                richParagraphList.remove(maxRichSpan.paragraph)
            } else if (minRichSpan.paragraph.getFirstNonEmptyChild() == null) {
                // Set the min paragraph type to the max paragraph type
                // Since the max paragraph is going to take the min paragraph's place
                maxRichSpan.paragraph.type = minRichSpan.paragraph.type

                // Remove the min paragraph if it's empty
                richParagraphList.remove(minRichSpan.paragraph)
            } else {
                // Merge the two paragraphs if they are not empty
                mergeTwoRichParagraphs(
                    firstParagraph = minRichSpan.paragraph,
                    secondParagraph = maxRichSpan.paragraph,
                )
            }
        }

        if (
            minRemoveIndex == minParagraphFirstChildMinIndex - minParagraphStartTextLength - 1
        ) {
            if (
                minRemoveIndex == minParagraphFirstChildMinIndex - minParagraphStartTextLength - 1 &&
                minParagraphStartTextLength > 0
            ) {
                val beforeText = tempTextFieldValue.text.substring(
                    startIndex = 0,
                    endIndex = minRemoveIndex,
                )
                val afterText = tempTextFieldValue.text.substring(
                    startIndex = minRemoveIndex + 1,
                    endIndex = tempTextFieldValue.text.length,
                )
                tempTextFieldValue = tempTextFieldValue.copy(
                    text = beforeText + afterText,
                    selection = TextRange(tempTextFieldValue.selection.min - 1),
                )
            }

            richParagraphList.getOrNull(minParagraphIndex - 1)?.let { previousParagraph ->
                // Merge the two paragraphs if the line break is removed
                mergeTwoRichParagraphs(
                    firstParagraph = previousParagraph,
                    secondParagraph = maxRichSpan.paragraph,
                )
            }
        }
    }

    checkOrderedListsNumbers(
        startParagraphIndex = minParagraphIndex - 1,
        endParagraphIndex = minParagraphIndex + 1,
    )
}


internal fun RichTextState.handleRemoveMinParagraphStartText(
    removeIndex: Int,
    paragraphStartTextLength: Int,
    paragraphFirstChildMinIndex: Int,
) {
    if (removeIndex >= paragraphFirstChildMinIndex || paragraphStartTextLength <= 0)
        return

    val indexDiff = paragraphStartTextLength - (paragraphFirstChildMinIndex - removeIndex)
    val beforeTextEndIndex = paragraphFirstChildMinIndex - paragraphStartTextLength

    val beforeText =
        if (beforeTextEndIndex <= 0)
            ""
        else
            tempTextFieldValue.text.substring(
                startIndex = 0,
                endIndex = beforeTextEndIndex,
            )
    val afterText =
        if (tempTextFieldValue.text.length <= removeIndex)
            ""
        else
            tempTextFieldValue.text.substring(
                startIndex = removeIndex,
                endIndex = tempTextFieldValue.text.length,
            )
    val newText = beforeText + afterText
    val newSelection = TextRange(removeIndex - indexDiff)

    tempTextFieldValue = tempTextFieldValue.copy(
        text = newText,
        selection = newSelection,
    )
}

internal fun RichTextState.handleRemoveMaxParagraphStartText(
    minRemoveIndex: Int,
    maxRemoveIndex: Int,
    paragraphStartTextLength: Int,
    paragraphFirstChildMinIndex: Int,
) {
    if (maxRemoveIndex < paragraphFirstChildMinIndex && paragraphStartTextLength > 0) {
        paragraphStartTextLength - (paragraphFirstChildMinIndex - maxRemoveIndex)

        val beforeText =
            if (minRemoveIndex <= 0)
                ""
            else
                tempTextFieldValue.text.substring(
                    startIndex = 0,
                    endIndex = minRemoveIndex,
                )

        val afterTextStartIndex = minRemoveIndex + (paragraphFirstChildMinIndex - maxRemoveIndex)

        val afterText =
            if (tempTextFieldValue.text.length <= afterTextStartIndex)
                ""
            else
                tempTextFieldValue.text.substring(
                    startIndex = afterTextStartIndex,
                    endIndex = tempTextFieldValue.text.length,
                )
        val newText = beforeText + afterText

        tempTextFieldValue = tempTextFieldValue.copy(
            text = newText,
        )
    }
}


@OptIn(ExperimentalRichTextApi::class)
private fun RichTextState.checkForParagraphs() {
    var index = tempTextFieldValue.text.lastIndex

    while (true) {
        // Search for the next paragraph
        index = tempTextFieldValue.text.lastIndexOf('\n', index)

        // If there are no more paragraphs, break
        if (index < textFieldValue.selection.min) break

        // Get the rich span style at the index to split it between two paragraphs
        val richSpan = getRichSpanByTextIndex(index)

        // If there is no rich span style at the index, continue (this should not happen)
        if (richSpan == null) {
            index--
            continue
        }

        // Get the paragraph style index of the rich span style
        val paragraphIndex = richParagraphList.indexOf(richSpan.paragraph)
        // If the paragraph index is -1, continue (this should not happen)
        if (paragraphIndex == -1) {
            index--
            continue
        }

        // Make sure the index is not less than the minimum text range of the rich span style
        // This is to make sure that the index is not in paragraph custom start text
        val sliceIndex = max(index, richSpan.textRange.min)

        // Create a new paragraph style
        val newParagraph = richSpan.paragraph.slice(
            startIndex = sliceIndex,
            richSpan = richSpan,
            removeSliceIndex = true,
        )

        // If the new paragraph is empty apply style depending on the config
        if (tempTextFieldValue.selection.collapsed && newParagraph.isEmpty()) {
            val newParagraphFirstRichSpan = newParagraph.getFirstNonEmptyChild()

            val isSelectionAtNewRichSpan =
                newParagraphFirstRichSpan?.textRange?.min == tempTextFieldValue.selection.min - 1

            // Check if the cursor is at the new paragraph and if it's an empty list item
            if (
                config.exitListOnEmptyItem &&
                isSelectionAtNewRichSpan &&
                richSpan.paragraph.isEmpty() &&
                richSpan.paragraph.type is ConfigurableListLevel
            ) {
                // Exit list by removing list formatting
                tempTextFieldValue = updateParagraphType(
                    paragraph = richSpan.paragraph,
                    newType = DefaultParagraphType(),
                    textFieldValue = tempTextFieldValue,
                )
                newParagraphFirstRichSpan.spanStyle = SpanStyle()
                newParagraphFirstRichSpan.richSpanStyle = RichSpanStyle.Default

                // Ignore adding the new paragraph
                index--
                continue
            } else if (
                (!config.preserveStyleOnEmptyLine || richSpan.paragraph.isEmpty()) &&
                isSelectionAtNewRichSpan
            ) {
                newParagraphFirstRichSpan.spanStyle = SpanStyle()
                newParagraphFirstRichSpan.richSpanStyle = RichSpanStyle.Default
            } else if (
                config.preserveStyleOnEmptyLine &&
                isSelectionAtNewRichSpan
            ) {
                newParagraphFirstRichSpan.spanStyle = currentSpanStyle
                newParagraphFirstRichSpan.richSpanStyle = currentRichSpanStyle
            }
        }

        // Get the text before and after the slice index
        val beforeText = tempTextFieldValue.text.substring(0, sliceIndex + 1)
        val afterText = tempTextFieldValue.text.substring(sliceIndex + 1)

        // Update the text field value to include the new paragraph custom start text
        tempTextFieldValue = tempTextFieldValue.copy(
            text = beforeText + newParagraph.type.startText + afterText,
            selection = TextRange(
                start = tempTextFieldValue.selection.start + newParagraph.type.startText.length,
                end = tempTextFieldValue.selection.end + newParagraph.type.startText.length,
            ),
        )

        // Add the new paragraph
        richParagraphList.add(paragraphIndex + 1, newParagraph)

        // Update the paragraph type of the paragraphs after the new paragraph
        val newParagraphType = newParagraph.type

        if (newParagraphType is OrderedList) {
            tempTextFieldValue = adjustOrderedListsNumbers(
                startParagraphIndex = paragraphIndex + 1,
                startNumber = newParagraphType.number,
                textFieldValue = tempTextFieldValue,
            )
        }

        // Remove one from the index to continue searching for paragraphs
        index--
    }
}